package model;

//TODO: log invalid broadcasts that could not be decrypted
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;

import util.RandomLogNormal;

import comm.UdpCommsPackage;

import constants.EObservationType;
import constants.ETimeToRemoveObservation;
import fileio.FileIo;

public class Node extends Observable implements Observer {

	//subcomponents
	private UdpCommsPackage comms;
	private FileIo fileIo;

	//Node identifiers
	private String ID = "";
	private Zone zone = null;
	private Position location;

	final private Map<Position,List<Observation>> observations=new HashMap<Position, List<Observation>>();
	final private Random randomGenerator = new Random();

	//log normal variables
	final private double standardDeviation = 0.5;
	private double mean = 1;
	private RandomLogNormal rln;
	private String basePath = "";
	private int moveCount =0;
	private int numberOfNodes = 0;
	private boolean verbose = true;
	
	public Node(Position location, Zone zone, String ID, String basePath, int mean, int numberOfNodes) throws IOException {
		this.location = location;
		this.zone  = zone;
		this.ID = ID; // use in log files
		this.mean = mean;
		this.basePath = basePath;
		this.numberOfNodes = numberOfNodes;
	}
	
	public Position getLocation(){
		return this.location;
	}
	
	//this section manages moveCount in order to handle possible threading issues and an inconsistent state
	private synchronized int getMoveCount(){
		return moveCount;
	}
	private synchronized void incrementMoveCount(){
		moveCount++;
	}

	protected synchronized void setMoveCountMax(){
		moveCount = Integer.MAX_VALUE-10;
	}
	
	public void run() throws IOException{
		logStartUpTime(this.location);
		
		Position tempLoc;

		
		this.fileIo = new FileIo(this.basePath + "/"
		                        + (int) this.numberOfNodes + "-"
								+ (int) this.mean + "-" + zone.getDimension() + "-"
				+ zone.getName()+"-"+ID + ".csv");
		
		rln = new RandomLogNormal(this.standardDeviation, this.mean);
		//setup UDP Comms
		try {
			this.comms = new UdpCommsPackage(zone.getName());
			this.comms.addObserver(this);
		} catch (Exception e) {
			writeErrorToFile("UDP Comms Exception at Constructor",e);
		}

		//main loop
		while ( getMoveCount()<30)// exit if node performed 30 moves
		{
			
			//remove old observations from local storage
			removeOldObservations();

			//determine if new observations are generated
			Observation obs = generateObservations();

			//if there are new observations
			if(obs.hasObservations()){
				
				//broadcast the new observations
				try {
					broadcastObservation(obs);
					if (verbose) System.out.println("Broadcasting: " +obs.getObservations().toString()+" at " 
							+this.location.getX()+", "+this.location.getY());
				} catch (Exception e) {
					this.writeErrorToFile("UDP Comms Error Broadcasting in run()", e);
				}
			}

			//check to see if the Node should move,
			//if the Node should move, determine where it should move
			tempLoc = move();
			if(tempLoc == null){
				if(CoinToss()){
					tempLoc =move();
				}

			}
			//if the Node should move, then set its location to the new location
			if(tempLoc!=null){	
				// Check for any observations in destination location

				//get the observations, including recording in instrumented node
				getObservationsAtLocation(tempLoc);
				
				incrementMoveCount();
				if (verbose) System.out.println("Moving from "+location.getX()+","+location.getY()+
						" to "+tempLoc.getX()+","+tempLoc.getY());
				if (verbose) System.out.println("This was move: "+getMoveCount());
				this.location = tempLoc;
			}

			//sleep according to the log normal
			try {
				Thread.sleep( (int) rln.retrieve() * 1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

		}	

		//when the Node is "Done", close the sockets and the file IO
		this.close();
	}

	protected synchronized void addObservation(Observation observation){
		//adds the observation to the local storage of observations

		//gets the list of observations associated with the location
		List<Observation> list = observations.get(observation.getLocation());

		//if the list is null
		if(list == null){
			//create a new list
			list = new ArrayList<Observation>(); 
		}

		//add the observation to the retrieved or new list
		list.add(observation);
		logAddObservation(observation.getLocation(),observation, list.size());

		//associate the list with the location again (should only be applicable to "new" locations)
		observations.put(observation.getLocation(), list);
	}

	protected void logAddObservation(Position location, Observation observation, int size) {
		// to be implemented in InstrumentedNode
		
	}

	@Override
	public void update(Observable arg0, Object object) {

		//checks for observation type
		if (object instanceof Observation) {

			//casts to observation
			Observation observation = (Observation) object;
						
			//confirms that the observation is within the view window
			if (isWithinView(observation.getLocation())) {

				//logs observations in the instrumented node
				logObservation(observation);
				
				EnumSet<EObservationType> enumSetObservations = observation.getObservations();
				
				// Check for medVac
				if(enumSetObservations.contains(EObservationType.MEDVAC_NEEDED)) {
					//write medvac in the Instrumented Node
					medvacDetected(observation);

					//remove the medvac from the EnumSet
					enumSetObservations.remove(EObservationType.MEDVAC_NEEDED); 
				} 

				// Save the observation to local storage
				if(!enumSetObservations.isEmpty()){
					addObservation(observation);
				}
				
			}

		} else if (object instanceof Message) {
			Message message = (Message) object;
			writeMessageToFile(message.getMessage(), location);
			if (message.getMessage().equalsIgnoreCase("Quit all Nodes")) {
				setMoveCountMax();
			}
		}
	}	
	protected void logObservation(Observation observation){
		// to be extended by InstrumentedNode
		
	}

	protected void medvacDetected(Observation observation) {
		// Used to record medvacs
		writeMessageToFile("Medvac,"+observation.getTimeStamp() + "," + observation.getObserver(), observation.getLocation());

	}

	protected boolean isWithinView(Position eventLocation){
		//returns true if the Location is within the view window
		//else returns false
		if (
				( Math.abs(location.getX() - eventLocation.getX()) <= zone.getHalfWindow() ) && 
				( Math.abs(location.getY() - eventLocation.getY()) <= zone.getHalfWindow() ) &&
				(location.getZone().equals(eventLocation.getZone()))
				){
			return true;
		}
		return false;
		
	}

	private void broadcastObservation(Observation obs) throws Exception{
		//broadcasts the observation
		
		//log the broadcast
		writeMessageToFile("Broadcasting: "+obs.getObservations().toString(), location);
		comms.broadcastSerializable(obs);
	}

	private boolean isOldObservation(long currentTime, long eventTime){
		
		long diff = currentTime - eventTime;
		if(diff> ETimeToRemoveObservation.TIME_TO_REMOVE_OBSERVATION.getTime()){
			return true;
		}
		return false;
	}

	private synchronized void removeOldObservations(){
		//removes "old" observations from the Node local storage

		List<Observation> list;
		Iterator<Observation> iterator;
		Observation o;
		long currentTime = System.currentTimeMillis();
		
		//for each key in the Observation Hashmap...
		for(Object key : this.observations.keySet()) {

			//get the list of observations 
			list = this.observations.get(key);
			iterator = list.iterator();

			//iterate through the list
			while(iterator.hasNext()){        
				o = iterator.next();

				//if an observation in the list is old...
				if(isOldObservation(currentTime, o.getTimeStamp())){
					//remove it
					iterator.remove();
				}

				else 
					break;
			}
		}
	}

	private synchronized Position move(){
		//generates a new move Location based on a random number generator 
		//returns the new Location if the location is valid
		//if the location is not valid, returns null
		int tempX = this.location.getX();
		int tempY = this.location.getY();
		int randomInt = randomGenerator.nextInt(10);
		// Where they're moving from #:
		// ------------- 
		if      ((randomInt == 0) || (randomInt == 6) || (randomInt == 7))  tempX = tempX - 1;    		// | 0 | 1 | 2 |
		else if ((randomInt == 2) || (randomInt == 3) || (randomInt == 4))  tempX = tempX + 1; 	   		// -------------
		// | 7 | # | 3 |
		if      ((randomInt == 4) || (randomInt == 5) || (randomInt == 6))  tempY = tempY - 1; 			// -------------
		else if ((randomInt == 0) || (randomInt == 1) || (randomInt == 2))  tempY = tempY + 1; 			// | 6 | 5 | 4 |
		// -------------
		// the second set of comparison needed to avoid moving to same location
		if (zone.validLocation(tempX, tempY) && tempX != this.location.getX() &&tempY != this.location.getY()) { 
			return new Position(tempX, tempY, this.zone.getName()); 
		}
		else { 
			return null; 
		}

	}   

	private synchronized Observation generateObservations(){
		//determines whether an observation is made
		EnumSet<EObservationType> Observation = EnumSet.noneOf(EObservationType.class);

		if (randomGenerator.nextInt(100) <= 02) {
			Observation.add(EObservationType.FIRE);
		}
		if (randomGenerator.nextInt(100) <= 30) {
			Observation.add(EObservationType.FLOODED_ROAD);
		}
		if (randomGenerator.nextInt(100) <= 05) {
			Observation.add(EObservationType.LOOTING);
		}
		if (randomGenerator.nextInt(100) <= 10) {
			Observation.add(EObservationType.SUPPLIES_AVAILABLE);
		}
		if (randomGenerator.nextInt(100) <= 05) {
			Observation.add(EObservationType.MEDVAC_NEEDED);
		}

		return new Observation(this.location, this.ID, Observation, System.currentTimeMillis());

	}

	protected synchronized void writeMessageToFile(String message, Position location){
		//writes a single line to file using timestamp,zone,id and a message
		
		//Runtime handle--retrieve memory values, etc
		Runtime runtime = Runtime.getRuntime();
		
		fileIo.write(System.currentTimeMillis()+","
				+ zone.getName()+","
				+ ID + ","
				+ mean + "," 
				+ this.numberOfNodes + ","
				+ zone.getWindowSize() + ","
				+ this.getMoveCount() + ","
				+ location.getX()+","
				+ location.getY()+","
				+ runtime.freeMemory() + ","
				+ message );
	}

	private void writeErrorToFile(String message, Exception e){
		//writes error messages to file
		writeMessageToFile(
				message+","
				+e.getMessage(), location);
	}
	
	public Map<Position, List<Observation>> getObservations() {
		return observations;
	}

	//returns a list containing all observations at the indicated location
	public synchronized List<EObservationType> getObservationsAtLocation(Position location){
		List<EObservationType> observations = new ArrayList<EObservationType>();
	
		//record time before getting observation
		logGetBeforeObs();
		
		//if the data structure has the location
		if(getObservations().containsKey(location)){
		
			//get the list of observations for the location
			List<Observation> tempList = getObservations().get(location);
			Iterator<Observation> it = tempList.iterator();
			
			//iterate through the list of observations
			while(it.hasNext()){
				Observation o = it.next();
				observations.addAll(o.getObservations());
			}
			
		}
		
		//log the observations from local storage 
		logGetAfterObs(location, observations.size());
		
		return observations;
	}	
	
	protected void logGetBeforeObs() {
		// To be implemented in Instrumented Node
		
	}

	protected void logGetAfterObs(Position location, int size) {
		// to be implemented in InstrumentedNode
		
	}

	protected void logStartUpTime(Position location) {
		// TODO Auto-generated method stub
		
	}

	
	/***************************************************************************
	 * CONCRETE METHOD:: CoinToss
	 * Purpose: This method provides a random true or false value used for
	 * determining the positiveness or negativeness of the drift value.
	 *
	 * Arguments: None.
	 *
	 * Returns: boolean
	 *
	 * Exceptions: None
	 *
	 ***************************************************************************/

	private boolean CoinToss()
	{
		//generates a random true/false
		Random r = new Random();
		return(r.nextBoolean());

	} // CoinToss

	public static void main(String[] args) {

		// args[0] is the zone name
		// args[1] is the zone dimension (square: dimension x dimension)
		// args[2] is the zone's "window" of interest
		// args[3] is the node's ID
		// args[4] is the base path
		// args[5] is the mean for log normal distribution
		// args[6] is the number of nodes running in zone

		Random random = new Random();
		
		Zone zone = new Zone(args[0], Integer.valueOf(args[1]), Integer.valueOf(args[2]));
		Position location = new Position ((int) (random.nextInt(zone.getDimension() - 1) + 1), 
				                          (int) (random.nextInt(zone.getDimension() - 1) + 1), 
				                          args[0]);
		try {
			Node node = new Node(location, zone, args[3], args[4], new Integer(args[5]), new Integer(args[6]));
			node.run();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private synchronized void close(){
		//closes the lower-level components
		
		writeMessageToFile("Closing Node - closing comms", location);
		comms.close();
		writeMessageToFile("Comms closed - closing fileIO", location);
		fileIo.close();
		Runtime.getRuntime().exit(0);
	}


}
